考量到安全問題,瀏覽器會以同源政策(Same-origin policy) 限制網頁對其他 Origin 的資源(Resource)存取,例如 AJAX、DOM、Cookie、圖片等等,然而透過 CORS 就能夠在滿足某些條件的情況下,突破同源政策限制取得其他 Origin 的資源。
網路上關於 CORS 的說明文章已經非常豐富,建議閱讀 MDN 的 Cross-Origin Resource Sharing (CORS)。本文將會對 CORS 進行重點整理,並加強說明文件中並未清楚解釋的部分,最後附上其他和 CORS 相關卻容易被忽略的觀念。
請參考 Same-origin policy,簡單來說,只要兩個網址的 Schema、Host、Port 皆相同就是 Same-origin,否則就是 Cross-origin。
CORS 分為簡單請求(Simple requests)和預檢請求(Preflighted requests)兩種,基本上能不能成功進行 CORS 都是看後端的造化,前端只能看著錯誤訊息請後端趕快修正。
Access to fetch at [url] from origin [origin] has been blocked by CORS policy: 理由
注意如果看到這段錯誤訊息代表 Request 已經正常送出並取得 Response,但因為違反 CORS policy,瀏覽器不讓 JavaScript 存取內容。
發出 CORS Request 時瀏覽器會自動在 Request header 加上目前的 Origin(假設是 http://example.com
),後端必須在 Response header 中加上相符的 Access-Control-Allow-Origin
才能完成 CORS:
Access-Control-Allow-Origin: * # 同意啦,哪次不同意
Access-Control-Allow-Origin: http://example.com # 只允許 http://example.com
如果 Request method 是GET
、HEAD
、POST
其一,且 Request header 的 Content-Type
是以下其中一種就是簡單請求,後端不需再做額外設定。
更詳細的規則請參考 Simple requests - MDN
只要不符合簡單請求的規則例如使用了 PUT
、DELETE
等 Method 或者 Content-Type
是 application/json
,在送出該 Request 之前,瀏覽器會先進行一次預檢(Preflight),和簡單請求不同的是如果沒有通過預檢,就不會發送 Request。
發送預檢請求時瀏覽器會先以 OPTIONS
method 問候一下後端:「我的 Origin 是 http:example.com
,我想要使用 PUT
method,另外還想帶上些客製化的 Header。」
OPTIONS /data HTTP/1.1
Origin: http://example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
後端收到 Request 後,可以任意決定要放行的設定:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: PUT, POST, GET, DELETE, OPTIONS
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 86400
只要 Request header 的 Origin
和 Access-Control-Request-
系列都在 Response header 的列表中,瀏覽器就會發出正式的 Reqeust,注意正式的 Response 依然要有符合的 Headers 才能完成 CORS。
至於 Access-Contorl-Max-Age
則是告訴瀏覽器幾秒之內不用再次預檢,以 86400
來說,就是完成一次預檢後有一天的效力。
預設 CORS Request 都是匿名(Anonymous)發送,因此想要帶上 Cookies 或是收到 Cookies 需要在前後端都加入一點設定。
前端以 Fetch 為例,無論是簡單還是預檢請求,加上一個設定值後瀏覽器在發出 CORS Request 時就會帶上 Cookies,同時 Response header 中的 Set-Cookie
才會生效:
// Frontend
fetch('https://example.com', { credentials: 'include' })
後端除了要滿足前面提及的 CORS policy 之外,還需要多加一條 Response header Access-Control-Allow-Credentials
,否則瀏覽器在收到 Response 時就會直接忽略掉,同時 Set-Cookie
header 也不會生效,另外 Access-Control-Allow-Origin
不能是 Wildcard(*
),需要和 Request 的 Origin 相同:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
在簡單請求的狀況下,收到正確 Response 後一切都會正常運作;在預檢請求的情況下,只有正式 Response 中的 Set-Cookie
才會生效。
做完以上準備後只是告知瀏覽器要傳送和接收 Cookies,但最終的是否實行還是會遵守 Cookie 的 SameSite
屬性,是否為 Cross-site 的判定方式在 Cookies - SameSite Attribute 中有詳細解釋。
SameSite
屬性為 Strict
或 Lax
時:在 Cross-origin 但 Same-site 的情況下加入 Credentials 設定就可以正常送出 Cookies。
如果是 Cross-origin 又 Cross-site,即使做了 Credentials 設定也會因為 Same-Site policy 無法送出 Cookies,需把 Cookie 的屬性設為 SameSite=None; Secure
才能送出。
Cross-site cookie 也稱作 Third-party cookie。
crossOrigin
屬性是否有遇過無法正常讀取字體或是想要取出 Canvas 的內容時出錯(The canvas has been tainted by cross-origin data
)的狀況呢?從錯誤訊息可以看出和 CORS 有關。
同樣是因為安全考量,取得這些 CORS 資源時必須把 crossOrigin
屬性設為 anonymous
告訴後端不要回傳 Credentials,同時也告訴瀏覽器這個資源沒有隱私問題,瀏覽器才會放心讀取資源,可以到 Demo 頁面試試看 CORS - Canvas。
img.crossOrigin = 'anonymous'
關於字體讀取的規範 Font fetching requirements:
<link rel="preload" href="awesome-font.woff2" as="font" type="font/woff2" crossorigin>
Cross-Origin Resource Sharing (CORS) - MDN
Cross-Origin Resource Sharing (CORS) - web.dev
發現手誤:
簡單請求
如果 Request method 是GET、GEAD、POST 其一
GEAD -> HEAD
謝謝,已修正囉